package ar.edu.unlu.sistemas.movirdroid.android;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import ar.edu.unlu.sistemas.movirdroid.business.exception.MovirdroidException;
import ar.edu.unlu.sistemas.movirdroid.business.exception.MovirdroidExceptionMessages;
import ar.edu.unlu.sistemas.movirdroid.business.index.IndexController;
import ar.edu.unlu.sistemas.movirdroid.business.index.Indexer;
import ar.edu.unlu.sistemas.movirdroid.business.msg.MessageInterpreter;
import ar.edu.unlu.sistemas.movirdroid.business.peer.Peer;
import ar.edu.unlu.sistemas.movirdroid.business.strategy.ProcessQueryStrategy;
import ar.edu.unlu.sistemas.movirdroid.business.strategy.impl.ProcessQueryLocalStrategyImpl;
import ar.edu.unlu.sistemas.movirdroid.business.strategy.impl.ProcessQueryRemoteStrategyImpl;
import ar.edu.unlu.sistemas.movirdroid.business.util.Constants;
import ar.edu.unlu.sistemas.movirdroid.business.util.Logger;
import ar.edu.unlu.sistemas.movirdroid.business.util.Params;
import ar.edu.unlu.sistemas.movirdroid.business.util.StringUtils;
import ar.edu.unlu.sistemas.movirdroid.business.vo.DocumentVO;
import ar.edu.unlu.sistemas.movirdroid.business.vo.TermFrecuencyVO;
import ar.edu.unlu.sistemas.movirdroid.business.vo.UpdateIndexVO;
import ar.edu.unlu.sistemas.movirdroid.business.vo.WordVO;
import ar.edu.unlu.sistemas.movirdroid.business.ws.MovirdroidWSInterface;
import ar.edu.unlu.sistemas.ws.business.QueryResultVO;
import ar.edu.unlu.sistemas.ws.business.ResourceDescriptionWS;

public class AppService extends Service {

	public static AppService instance;
	
	private IndexController index;
	
	private MovirdroidWSInterface wsInterface;
	
	private Peer peer;
	
	public boolean indexUpdateLaunched = false;
	
	private static final int NOTIFICATION_ID_STA = 43;
	private Notification notification;  
    
	public void onCreate() {
		AppService.instance = this;
		Thread serviceInitThread = new Thread() {
			public void run() {
				//INICIALIZO CONFIGURACION
				initConfig();
				peer = new Peer();
				peer.initConnection(getipAddress());
				
				//INICIALIZO LOS MODULOS NECESARIOS
				index = IndexController.getInstance();
				
				if (Params.BROKER_ENABLED) {
					wsInterface = new MovirdroidWSInterface(peer);
					
					//ME CONECTO CON EL WEBSERVICE
					wsInterface.sayHello();
					wsInterface.putIndex(index.getResourceDescription());
				}

				//CREO Y LANZO LA NOTIFICACION CUANDO INICIA LA APLICACION
				getApplicationContext().getSystemService(
		    			getApplicationContext().NOTIFICATION_SERVICE);
				CharSequence charsec = "Movirdroid"; 
				// configure the notification
		    	notification = new Notification(R.drawable.icon, charsec, System
		    			.currentTimeMillis());
		    	// The PendingIntent to launch our activity if the user selects this notification
		        PendingIntent contentIntent = PendingIntent.getActivity(AppService.instance, 0,
		                new Intent(AppService.instance, QueryActivity.class),PendingIntent.FLAG_UPDATE_CURRENT);
		        notification.setLatestEventInfo(AppService.instance, charsec,
		                charsec, contentIntent);
				startForeground(NOTIFICATION_ID_STA, notification);
				sendBroadcast(new Intent(Constants.BOOT_COMPLETE_INTENT));
			}
		};
		serviceInitThread.start();
	}
	
	//ESTO ES PERTINENTE AL BINDE DEL SERVICIO CON LAS ACTIVIDADES
	//INICIO
	private final IBinder mBinder = new LocalBinder();
	public class LocalBinder extends Binder {
        AppService getService() {
            return AppService.this;
        }
    }
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		//Crea el directorio en la memoria SD
		this.getDirectory();
		return START_STICKY;
	}
	@Override
	public IBinder onBind(Intent arg0) {
		return mBinder;
	}
	@Override
	public void onDestroy() {
	    super.onDestroy();
	}
	//FIN
	
	/**
	 * Inicializa la configuración levantando el xml
	 */
	private void initConfig() {
		try {
			Params.loadPreferences(this.openFileInput(Constants.CONFIG_FILE_PATH));
		} catch (MovirdroidException me) {
			Logger.logError(this.getClass().getSimpleName(),me.getMessage(),me);
		} catch (FileNotFoundException fnfe) {
			try {
				Logger.logInfo(this.getClass().getSimpleName(),"No existe el archivo de config. Se crea uno");
				Params.savePreferences(this.openFileOutput(Constants.CONFIG_FILE_PATH,Context.MODE_PRIVATE));
			} catch (MovirdroidException me) {
				Logger.logError(this.getClass().getSimpleName(),me.getMessage(),me);
			} catch (FileNotFoundException fnfe2) {
				Logger.logError(this.getClass().getSimpleName(),MovirdroidExceptionMessages.ERROR_FINDING_CONFIG,fnfe2);
			}
		}
	}
	
//MÉTODOS PARA ACTIVIDAD DE CONSULTA DEL ÍNDICE
	
	/**
	 * Va a buscar al índice los términos que subcontienen la palabra pasado por parametro
	 * @param word
	 * @return colección de value-objects word
	 */
	public Collection<WordVO> findWords(String word) {
		if (!StringUtils.esStringVacioONull(word)) {
			Log.i(this.getClass().getSimpleName(), "Voy a buscar el termino: "+word);
			return this.index.findWordsLikeName(word);
		} else {
			Log.i(this.getClass().getSimpleName(), "Voy a buscar todos los terminos");
			return this.index.findAllWords();
		}
	}
	
	/**
	 * Va a buscar al índice los documentos cuyo nombre subcontienen al pasado por parametro
	 * @param documento
	 * @return colección de value-objects document
	 */
	public Collection<DocumentVO> findDocs(String doc) {
		if (!StringUtils.esStringVacioONull(doc)) {
			Log.i(this.getClass().getSimpleName(), "Voy a buscar el documento: "+doc);
			return this.index.findDocsLikeName(doc);
		} else {
			Log.i(this.getClass().getSimpleName(), "Voy a buscar todos los documentos");
			return this.index.findAllDocs();
		}
	}
	
	
//MÉTODOS PARA ACTIVIDAD DE CONSULTA (ACTIVIDAD PRINCIPAL)
	
	private class QueryThread extends Thread {
		private Handler activtyHandler;
		private String query;
		
		public QueryThread(String query, Handler activtyHandler) {
			this.query = query;
			this.activtyHandler = activtyHandler;
		}
		
		public void run() {
			Logger.logInfo(AppService.class.getName(), "Realizo una query");
			Collection<QueryResultVO> results;
			if (Params.BROKER_ENABLED) {
				results = wsInterface.query(query);
				results.addAll(processQueryLocal(query));
			} else results = processQueryLocal(query);
			activtyHandler.sendMessage(activtyHandler.obtainMessage(1, results));
		}
	}
	
	/**
	 * Dispara el evento de la realización de una query. La lógica es análoga a la del ping.
	 * 
	 * @param la query representada por un string
	 * @return el id del mensaje ping
	 */
	public void doQuery(String query, Handler activtyHandler) {
		QueryThread qThread = new QueryThread(query,activtyHandler);
		qThread.start();
	}

	/**
	 * Procesa la query localmente. En este caso usa una estrategia estándar
	 * @param query
	 * @return
	 */
	public Collection<QueryResultVO> processQueryLocal(String query) {
		Logger.logInfo(this.getClass().getSimpleName(),"Realizo consulta: "+query);
		ProcessQueryStrategy strategy;
		strategy = new ProcessQueryLocalStrategyImpl();
		return strategy.execute(query);
	}
	
	/**
	 * Procesa la query localmente. En este caso usa una estrategia estándar
	 * @param query
	 * @return
	 */
	public Collection<QueryResultVO> processQueryRemote(String query) {
		Logger.logInfo(this.getClass().getSimpleName(),"Realizo consulta: "+query);
		ProcessQueryStrategy strategy;
		strategy = new ProcessQueryRemoteStrategyImpl();
		return strategy.execute(query);
	}
	
	/**
	 * Retorna el archivo cuando se clickea en la lista
	 * Si es local abre el archivo sino lo pide al host correspondiente, alamace y abre
	 * @param fileName
	 * @return
	 */
	public File getFile(QueryResultVO document) {
		//TODO: SI EL RESULTADO NO ES LOCAL IR A BUSCARLO AL HOST CORRESPONDIENTE
		File file = null;
		if (Constants.SD_UNMOUNTED != this.getSDCardState()) {
			file = new File(this.getDirectory(), document.getPath());
		}
		return file;
	}
	
	
//ACTUALIZACION DE ÍNDICE
	
		public final Handler mHandler = new Handler() {
			//Se envían mensajes al handler que realizarán una acción
			//determinada por este método debido a que otro thread que no
			//sea el creador de la UI puede modificarla
			//Este código se ejecuta en el main thread
			public void handleMessage(Message msg) {
				super.handleMessage(msg);
				switch(msg.what) {
					case 0:
						this.post(mShowToast);
						break;	
					default:
						break;
				}
			}
		};
		
		final Runnable mShowToast = new Runnable() {
	        public void run() {
	        	showToast();
	        }
	    };
	    
	    public void showToast() {
	    	Toast toast = Toast.makeText(this,"Indice actualizado "+indexTimeMillis+"ms", Toast.LENGTH_LONG);
	    	toast.show();
	    }
	    
	    public void notifyIndexingProgress(int maxProgress, int currentProgress) {
	    	
	        Intent intent = new Intent(Constants.INDEXING_INTENT);
	        intent.putExtra("MAX_PROGESS", maxProgress);
	        intent.putExtra("CURRENT_PROGRESS",currentProgress);
	        this.sendBroadcast(intent);
	    }
	    
	    /**
	     * Inicia la indexación en modo asincrónico
	     */
	    public void updateIndexAsinc() {
	    	this.indexUpdateLaunched = true;
	    	final Thread thread = new Thread() {
	    		public void run() {
	    			updateIndex();
	    		}
	    	};
	    	thread.start();
	    }
	    
	    /**
	     * Verifica si el índice está actualizado
	     * @return
	     */
	    public boolean isIndexUpdated() {
	    	Collection<UpdateIndexVO> list = this.getUpateActions();
	    	return ((list != null) && (list.isEmpty()));
	    }
	    
	    /**
	     * Verifica si se lanzo la actualización del índice
	     * @return
	     */
	    public boolean isIndexUpdateLaunched() {
	    	return this.indexUpdateLaunched;
	    }
	    
	    /**
		 * Retorna una lista de acciones a realizar para actualizar el índice
		 * @return lista de acciones de actualizacion
		 */
		private Collection<UpdateIndexVO> getUpateActions() {
			if (Constants.SD_MOUNTED_WRITE == this.getSDCardState()) {
				File dir = this.getDirectory();
				List<String> filesList = Arrays.asList(dir.list());
				Log.i(this.getClass().getSimpleName(), "Archivos en el directorio "+filesList.toString());
				Collection<String> docs = (Collection<String>) this.index.findAllDocsNames();
				Log.i(this.getClass().getSimpleName(), "Archivos indexados "+docs.toString());
				Collection<String> docNames = new ArrayList<String>();
				Iterator<String> docsIt = docs.iterator();
				String tempDocName;
				Collection<UpdateIndexVO> outFiles = new ArrayList<UpdateIndexVO>();
				//Primero obtengo aquellos documentos que ya no están en el directorio para borrar del indice
				while (docsIt.hasNext()) {
					tempDocName = docsIt.next();
					docNames.add(tempDocName);
					if (!filesList.contains(tempDocName)) {
						outFiles.add(UpdateIndexVO.removeDoc(tempDocName));
					}
				}
				Iterator<String> fileListIt = filesList.iterator();
				//Luego identifico aquellos archivos del directorio que no están como documentos
				while (fileListIt.hasNext()) {
					tempDocName = (fileListIt.next());
					if (!docNames.contains(tempDocName)) outFiles.add(UpdateIndexVO.insertDoc(tempDocName));
				}
				Log.i(this.getClass().getSimpleName(), "Acciones para actualizar "+outFiles.toString());
				return outFiles;
			} else {
				return null;
			}
			
		}
	    private Long indexTimeMillis;
	    /**
	     * Actualiza el índice según los archivos que existen en la carpeta en determinado instante.
	     * Si se encuentra un nuevo archivo se indexa el documentos que contienen el mismo.
	     * Si algún archivo ya no existe, se remueve del índice.
	     */
	    private void updateIndex() {
	    	Collection<UpdateIndexVO> actions = this.getUpateActions();
			if (actions != null && (actions.size()>0)) {
				//Tiempo de indexacion				
				Long begin = System.currentTimeMillis();
				//MUESTRO LA BARRA DE PROGRESO DE LA INDEXACION
				int maxProgress = actions.size();
				this.notifyIndexingProgress(maxProgress,0);
				int i = 0;
				//ITERO LOS ARCHIVOS Y CONFORME LO QUE HAYA QUE HACER INDEXO UNO NUEVO O ELIMINO UNO EXISTENTE
				Iterator<UpdateIndexVO> actionsIt = actions.iterator();
				UpdateIndexVO updateActionVO;
				Map<String,Integer> resourceDelta = new HashMap<String,Integer>();
				Integer nDocs = 0;
				Set<String> tfsAdded;
				Collection<TermFrecuencyVO> tfsRemoved;
				while (actionsIt.hasNext() && (Constants.SD_UNMOUNTED != this.getSDCardState())) {
					updateActionVO = actionsIt.next();
					if (UpdateIndexVO.ACTION_INSERT.equals(updateActionVO.getAction())) {
						//TRATO DE INDEXAR EL DOCUMENTO
						if (Constants.SD_UNMOUNTED != this.getSDCardState()) {
							File file = new File(this.getDirectory(), updateActionVO.getDocName());
							if (file != null && file.exists()); {
								try {
									FileReader fr = new FileReader(file);
									tfsAdded = Indexer.getInstance().addFile(fr, updateActionVO.getDocName());
									for (String word : tfsAdded) {
										if (!resourceDelta.containsKey(word)) resourceDelta.put(word, 1);
										else resourceDelta.put(word,  resourceDelta.get(word) + 1);
									}
									nDocs++;
								} catch (FileNotFoundException fnfe) {
									Logger.logError(MessageInterpreter.class.getSimpleName(), " Error agregando "+file.getName(),fnfe);
								}
							}
						}
					} else {
						//REMUEVO EL DOCUMENTO
						tfsRemoved = this.index.removeDoc(updateActionVO.getDocName());
						Iterator<TermFrecuencyVO> tfsRemovedit = tfsRemoved.iterator();
						TermFrecuencyVO tf;
						while (tfsRemovedit.hasNext()) {
							tf = tfsRemovedit.next();
							if (!resourceDelta.containsKey(tf.getWord().getName())) resourceDelta.put(tf.getWord().getName(), -1);
							else resourceDelta.put(tf.getWord().getName(),  resourceDelta.get(tf.getWord().getName()) - 1);
						}
						nDocs--;
					}
					//ACTUALIZO LA BARRA DE NOTIFICACION
					this.notifyIndexingProgress(maxProgress,i++);
				}
				this.index.saveIndex();
				if (Params.BROKER_ENABLED) {
					//NOTIFICO AL SERVER
					ResourceDescriptionWS resourceDescription = new ResourceDescriptionWS();
					resourceDescription.setResourceDescription(resourceDelta);
					resourceDescription.setnDocs(nDocs);
					this.wsInterface.updateIndex(resourceDescription);
					
				}
				//CIERRO BARRA DE PROGRESO
				this.notifyIndexingProgress(maxProgress,maxProgress);
				//ENVIO MENSAJE PARA QUE APAREZCA UN CARTELITO QUE DIGA INDICE ACTUALIZADO
				indexTimeMillis  = System.currentTimeMillis()-begin;
				this.mHandler.sendMessage(this.mHandler.obtainMessage(0));
			}
			this.indexUpdateLaunched = false;
		}
    
//MANEJO DE PREFERENCIAS
    
	    /**
	     * Levanta la configuración
	     * @return un Map con las preferencias cargadas
	     */
	    public Map<String,String> loadPreferences() {
	    	return Params.getMapPreferences();
	    }
	    
	    /**
	     * Gurada la configuración en archivo
	     * @param Map con la configuración
	     */
	    public void savePreferences(Map<String,String> prefMap) {
	    	try {
	    		if (prefMap != null) Params.setMapPreferences(prefMap);
	    		Params.savePreferences(this.openFileOutput(Constants.CONFIG_FILE_PATH, Context.MODE_PRIVATE));
	    		Logger.logInfo(this.getClass().getSimpleName(),"Se persistio la configuracion");	
	    	} catch (MovirdroidException me) {
					Logger.logError(this.getClass().getSimpleName(),me.getMessage(),me);
				} catch (FileNotFoundException fnfe2) {
					Logger.logError(this.getClass().getSimpleName(),MovirdroidExceptionMessages.ERROR_FINDING_CONFIG,fnfe2);
				}
	    }
    
    
    
//UTILIDADES
    
		/**
		 * Obtiene el directorio donde se ubican los archivos a indexar.
		 * Si el directorio no existe lo crea previamente.
		 * @return File (directorio)
		 */
		private File getDirectory() {
			File rootDir = Environment.getExternalStoragePublicDirectory(Params.CORPUS_DIR);
			if (!rootDir.exists()) {
				rootDir.mkdirs();
			}
			return rootDir;
		}
	    
	    /**
	     * UTILIDAD QUE VERIFICA EL ESTADO DE LA TARJETA SD
	     * @return
	     */
	    public int getSDCardState() {
	
			String state = Environment.getExternalStorageState();
	
			if (Environment.MEDIA_MOUNTED.equals(state)) {
				// We can read and write the media
				return Constants.SD_MOUNTED_WRITE;
			} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
				// We can only read the media
				return Constants.SD_MOUNTED_READ;
			} else {
				// Something else is wrong. It may be one of many other states, but all we need
				//  to know is we can neither read nor write
				return Constants.SD_UNMOUNTED;
			}
		}
		
	    /**
	     * UTILIDAD QUE RETORNA LA IP DE LA INTERFAZ 3G O WIFI
	     * @return
	     */
		public String getipAddress() {
	        try {
	            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
	                NetworkInterface intf = en.nextElement();
	                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
	                    InetAddress inetAddress = enumIpAddr.nextElement();
	                    if (!inetAddress.isLoopbackAddress()) {
	                        String ipaddress=inetAddress.getHostAddress().toString();
	                        Logger.logInfo(this.getClass().getSimpleName(), "ip address "+ipaddress);
	                        return ipaddress;
	                    }
	                }
	            }
	        } catch (SocketException ex) {
	            Logger.logError(this.getClass().getSimpleName(),"Error obteniendo IP de interface", ex);
	        }
	        return null;
	    }
    
}
